feat(phase5): contested status for close-call conflict resolution#166
Merged
salishforge merged 1 commit intoJul 26, 2026
Merged
Conversation
The final Phase 5 slice — the Feature 1 remnant that never landed with PR #137. Phase 2.5 conflict resolution stops encoding noise as fact. - When the multi-factor score gap between two conflicting live memories is at most one point (one factor's worth of noise on the quantized integer scale), both are marked epistemic_status='contested' and the conflict resolves with a contested(...) strategy and no winner. The umbrella used a relative 20% threshold, which inverts on small integer scores (pure-noise 1-vs-0 reads as decisive, well-evidenced 6-vs-5 as contested) and is distorted by the mutual +100 supersession offset — the absolute delta is immune to both. - Superseded rows never contest: supersession is an absolute signal, and marking stale facts contested would resurface them as live disputes. - Contested is not terminal (the trap this same stack fixed for 'inferred'): Phase 5.12 promotes contested rows that earn the corroboration bar, and a decisive re-resolution of the dispute clears the winner's badge back to provisional. - tests/contested-conflicts.test.ts: 9 tests — close-call semantics, the exact one-vs-two-point boundary, winner/loser path preservation incl. untouched winner confidence, both-superseded decisiveness, contested recovery via decisive re-resolution. Conflict pairs share an identical relative timestamp (separate INSERTs see different now(), silently handing one side the +3 recency factor). - tests/epistemic-confidence.test.ts: the promotion tests now pin the new behavior (corroborated contested rows promote; uncorroborated ones stay). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru
This was referenced Jul 26, 2026
Owner
Author
|
CI status: all 11 checks fail in ~5-30s with an Fix: #167 pins |
salishforge
added a commit
that referenced
this pull request
Jul 26, 2026
* feat(phase5): explainable memory operations (v3.10) Feature 2 of the Phase 5 split (umbrella #129): every retrieval can say why, and any warm memory can be asked where it stands. - query(): opt-in explain=true attaches ExplanationFactor[] per result (rank_score, epistemic_status weight, search_mode, temporal_decay) - explainMemory() + GET /memory/:id/explain?warm_id= reports scores, access patterns, and standing against the sleep-cycle score thresholds. Flags are scoped honestly (would_evict_by_threshold, would_flag_low_confidence): capacity eviction and the outcome / contradiction revision channels depend on live cross-table state and are deliberately not predicted here. - memforge_explain MCP tool wired to the real client method; TS + Python SDKs including both resilient wrappers; OpenAPI entries - query cache key extracted to queryKey() in cache.ts so the explain flag's participation in the key is unit-testable without Redis - warm_id validated to int8 range at REST and MCP boundaries; invalid agent ids on /explain return 400 like sibling routes No schema migration — pure code feature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru * feat(phase5): causal memory graph (v3.10) Feature 3 of the Phase 5 split (umbrella #129): MemForge learns which events tend to follow which, and can answer "what led here" and "what happens next". - causal_edges table (migration-v3.10.sql + canonical schema, RLS, agent-scoped FKs to warm_tier with CASCADE) - Sleep Phase 6.1 mines memory_sequences for repeated content-level A→B patterns (md5 of 50-char prefixes, the Phase 5.5 technique) — the umbrella's row-pair grouping was unsatisfiable under the table's UNIQUE constraint and could never mine an edge. One edge per pattern, anchored to the earliest surviving pair so re-mining updates in place and FK cascade self-heals evicted anchors. Not skip-gated: the phase's input accumulates independently of its own change history, so the zero-change gate would permanently disable mining for young agents. Count surfaces as SleepCycleResult.causal_edges_updated. - getCausalChain(): recursive CTE traversal (causes|effects, depth clamped 1-10, 50-row cap, ordered depth then strength) - predict(): context match → outgoing edges → ranked events. probability = confidence × (1 − e^(−strength/30)) — monotonic, never saturated (raw strength × confidence clamps to 1.0 for virtually every mined edge). Shared-pool matches are excluded from the cause-id list (different id space than warm_tier), and effects are confined to the requested namespace. - GET /memory/:id/causal + POST /memory/:id/predict, memforge_causal_chain + memforge_predict MCP tools wired to real client methods, TS + Python SDKs incl. resilient wrappers, tool-definitions, OpenAPI - tests/causal-graph.test.ts: 39 tests (traversal incl. cycles/clamps/ cap/ordering, predict incl. namespace + pool-collision, Phase 6.1 mining incl. cv weighting + anchor stability + true re-confirmation step, pruning, HTTP status paths, migration shape + RLS) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru * feat(phase5): hierarchical abstraction engine (v3.11) Feature 4 of the Phase 5 split (umbrella #129): Sleep Phase 5.11 distills cross-cutting principles from accumulated meta-reflections into a queryable abstractions store. - abstractions table (migration-v3.11.sql + canonical schema): level CHECK (principle|strategy|mental_model), stored md5 content_hash with UNIQUE (agent_id, level, namespace, content_hash) — the umbrella's ON CONFLICT DO NOTHING had no conflict target and would have duplicated every principle every cycle. RLS + grants. - Phase 5.11: gated on LLM presence, token budget, and >= 3 meta-reflections; prompt carries the codebase-standard injection guard, wraps reflection content via wrapUserContent, and states the schema's own limits (max 10, under 2000 chars, empty-array escape) so one overlong item does not sink the batch silently. Re-derivation is corroboration: ON CONFLICT keeps GREATEST confidence and revives a deactivated principle only when the fresh derivation is confident (>= 0.5); the conditional WHERE keeps change counts honest. Age-based deactivation of stale sub-0.2 principles. Not skip-gated (input accumulates independently). Cancellation checkpoint before the LLM call so canceled dream runs stop paying. runPhase now records real per-phase token spend in sleep_phase_analytics; count surfaces as SleepCycleResult.principles_extracted. - getAbstractions()/getPrinciples(), GET /principles + /abstractions routes, memforge_principles + memforge_mental_models MCP tools wired to real client methods (honest description: only 'principle' is auto-extracted today), TS + Python SDKs incl. resilient wrappers, tool-definitions, OpenAPI with shared Abstraction component. - tests/abstractions.test.ts: 39 tests — read paths, extraction incl. dedup/revival/confidence-raise/skip-gate immunity/token attribution/ budget + window gates/malformed + schema-invalid + missing-key LLM output, deactivation age and scope boundaries, ordering tiebreak, HTTP status paths, migration shape + RLS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru * feat(phase5): cross-agent transfer learning bootstrap (v3.12) Feature 7 of the Phase 5 split (umbrella #129), the final feature: bootstrap a new agent from an experienced one. - bootstrapAgent(): one atomic transaction copying established warm memories (as epistemic 'inferred', 0.5x importance/confidence, _transferred_from provenance), active procedures, and active principles from source to target. Set-based INSERT..SELECT with dedup BEFORE each LIMIT so already-carried knowledge frees its cap slot (the umbrella looped N+1 with no-op ON CONFLICTs — procedures has no unique constraint at all). Memory dedup is namespace-scoped and content-exact (organic rows hash content differently, so equality is the only honest check). Warm-tier creates carry hash-chained audit records ('bootstrap' trigger) like consolidation and cold-restore do. - Phase 5.12 promotion now also considers 'inferred' rows meeting the same evidence bar — bootstrap is the first mass writer of inferred status, which was previously terminal: excluded by every epistemic filter except 'all' with no path out regardless of corroboration. - POST /memory/:id/bootstrap (path agent = target; Zod-validated body incl. agent-id regex and 0-1000/0-100 caps), memforge_bootstrap MCP tool with its own agent-pair validation and a real executor, TS + Python SDKs incl. resilient wrappers, tool-definitions, OpenAPI. Docs state honestly: principles carry no provenance marker (no metadata column); transferred rows are FTS-searchable immediately, semantic after embedding backfill, filter-visible after promotion. - No schema migration (audit trigger comment updated in schema.sql). - tests/bootstrap.test.ts: 22 tests — transfer semantics per category, discounts + provenance, namespace scoping incl. the cross-namespace dedup regression, per-category caps incl. principle slot-starvation regression, idempotency, organic-content dedup, transaction rollback via sabotage trigger, inferred->established promotion, auto-register, HTTP status paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru * feat(phase5): contested status for close-call conflict resolution (#166) The final Phase 5 slice — the Feature 1 remnant that never landed with PR #137. Phase 2.5 conflict resolution stops encoding noise as fact. - When the multi-factor score gap between two conflicting live memories is at most one point (one factor's worth of noise on the quantized integer scale), both are marked epistemic_status='contested' and the conflict resolves with a contested(...) strategy and no winner. The umbrella used a relative 20% threshold, which inverts on small integer scores (pure-noise 1-vs-0 reads as decisive, well-evidenced 6-vs-5 as contested) and is distorted by the mutual +100 supersession offset — the absolute delta is immune to both. - Superseded rows never contest: supersession is an absolute signal, and marking stale facts contested would resurface them as live disputes. - Contested is not terminal (the trap this same stack fixed for 'inferred'): Phase 5.12 promotes contested rows that earn the corroboration bar, and a decisive re-resolution of the dispute clears the winner's badge back to provisional. - tests/contested-conflicts.test.ts: 9 tests — close-call semantics, the exact one-vs-two-point boundary, winner/loser path preservation incl. untouched winner confidence, both-superseded decisiveness, contested recovery via decisive re-resolution. Conflict pairs share an identical relative timestamp (separate INSERTs see different now(), silently handing one side the +3 recency factor). - tests/epistemic-confidence.test.ts: the promotion tests now pin the new behavior (corroborated contested rows promote; uncorroborated ones stay). Claude-Session: https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The final Phase 5 slice — the Feature 1 remnant that never landed with #137. Stacked on #165 → #164 → #163 → #160.
Phase 2.5 conflict resolution now refuses to pick a winner on noise: a score gap of at most one point between two live memories marks both
contested(no winner, no loser penalty). Three deliberate departures from the umbrella reference, all driven by the adversarial review:inferred(the review caught it as a high-severity finding: this diff is the codebase's first-ever writer ofcontested).Review process
Two-lens adversarial review workflow (8 agents): 6 raw findings → 6 confirmed (0 rejected — an unusually accurate review), all fixed, including two test-design flaws (a wall-clock-dependent pinned date, and a degenerate close-call fixture that separate-INSERT
now()drift would have scored 1-vs-3 anyway).Test evidence
npm run type-check/npm run linttests/contested-conflicts.test.ts(new)npm run test:epistemic-confidence(2 tests updated to pin the new promotion behavior)npm run test:multi-devicenpm run test:integration🤖 Generated with Claude Code
https://claude.ai/code/session_011xCqQo49d3CEbn6oEvb3Ru